<?php
/**
* ActiveRecord
* Класс ActiveRecord является базовым классом для всех объектов БД
* @author Mike
*/
abstract class ActiveRecord {
	protected $tableName;   				// Название таблицы в БД
	protected $pkName;						// Название первичного ключа в БД
	protected $arrSettings;					// Массив дополнительных свойств объекта
	protected $requiredFields = array(); 	// Обязательные для заполнения поля
	protected $fieldsRules = array();    	// Массив правил для заполнения полей (name,mail,number,date)
	public $tableFields = array ();			// Массив полей, загружаемых в объект
	public $id;								// Первичный ключ объекта
	protected $modelName;					// Имя модели объекта
 	protected $saveType;
	
	protected $arrErrors = array();
	
	private $stmt;
	
	private $select = '*';
	private $conditions = null;
	private $joins = null;
	private $order = null;
	private $limit = null;
	private $group = null;
	private $having = null;
	
	
	public function __construct($initialFields = null) {
		if (is_numeric($initialFields)) {
			// Выполняем поиск в БД записи с $id и при наличии таковой сохраняем данные в объект
			$this->findOne($initialFields);
			if (isset($this->tableFields[$this->pkName])) {
				$this->id = $this->tableFields[$this->pkName];
			}
		}
		else if (is_array($initialFields)) {
			$this->tableFields = &$initialFields;
			if(!empty($this->tableFields[$this->pkName])) $this->id = $this->tableFields[$this->pkName];
		}
	}
		
	/**
	 * Метод поиска одного элемента
	 *
	 * @param integer $idToFind - id для поиска
	 * @return array
	 */
	protected function findOne($idToFind) {
		$objDB = Service_Database::instanse();  // Получаем подключение к БД
		$this->conditions = "`" . $this->tableName . "`.`" . $this->pkName . "` = '" . $idToFind . "'"; // настраиваем поиск по айдишнику
		// устанавливаем умолчания на случай, если через этот объект уже осуществляли поиск
		$this->limit = 1;
		// осуществляем поиск и сохранение результатов
		$this->_makeStmt();
		// echo $this->stmt . '<br>';
		$this->tableFields = $objDB->selectOneRow($this->stmt);
		$this->id = isset($this->tableFields[$this->pkName]) ? $this->tableFields[$this->pkName] : null;
		
		// Возвращаем сформированный результат
		return $this->tableFields;
	}
	
/**
   * Метод находит в БД объект/объекты
   *
   * @param string $itemsToFind - тип поиска - число (id)/all/first
   * @param array $arrSettings - свойства иска
   * @return array
   */
	public function find($itemsToFind, $arrSettings = array()) {
		// устанавливаем умолчания на случай, если через этот объект уже осуществляли поиск
  		$this->limit = null;
		$this->conditions = null;
		$this->select = '*';
		$this->joins = null;
		$this->order = null;
		$this->having = null;
		$this->group = null;		

		// Устанавливаем все поисковые свойства
		foreach ($arrSettings as $settingName => $settingValue) {
			$this->setSetting($settingName, $settingValue);
  		}

  		// Определяем тип поиска и делаем соответствующий вызов
		if (is_numeric($itemsToFind)) return $this->findOne($itemsToFind);
		elseif ($itemsToFind == 'all') return $this->findAll();
		elseif ($itemsToFind == 'first') return $this->findFirst();
		else throw new Exception('The first parameter to the find method should be either an identifier or "all", or "first". You gave "' . $itemsToFind . '"');
	}
	
	/**
	 * Метод поиска списка элементов
	 *
	 * @return array
	 */
	private function findAll() {
		$objDB = Service_Database::instanse();// Получаем подключение к БД
		$this->_makeStmt();		
		$arr = $objDB->select($this->stmt);	// Получаем данные об основной сущности вместе с данными о сопутствующих элементах
		return 	$arr;		
	}
	
	/**
	 * Метод поиска первого элемента в списке
	 *
	 * @return array
	 */
	private function findFirst() {
		$objDB = Service_Database::instanse();// Получаем подключение к БД
		// устанавливаем умолчания на случай, если через этот объект уже осуществляли поиск
		$this->limit = 1;	
		// осуществляем поиск и сохранение результатов
		$this->_makeStmt();
		//echo $this->stmt;
		$this->tableFields = $objDB->selectOneRow($this->stmt);
		$this->id = $this->getField($this->pkName);
	
		// Возвращаем сформированный результат
		return $this->tableFields;
	}
	
	/**
	 * Метод сохранения текущего обекта в БД
	 * 
	 * @return bool - флаг успешности сохранения 
	 */
	public function save() {	
		// Устанавливаем тип сохранения	
		if($this->id) {
			$this->saveType = 'update';
			$this->beforeValidationOnUpdate();
		}
		else  {
			$this->saveType = 'create';
			$this->beforeValidationOnCreate();
		}
		$this->beforeValidation();
		$this->validation();

		// Если массив ошибок пуст, сохраняем объект
		if (empty($this->arrErrors)) {
			
			if($this->saveType == 'create') {
				$this->beforeCreate();
				if($isSaved = $this->add()) $this->afterCreate();
				
			}
			elseif($this->saveType == 'update') {
				$this->beforeUpdate();
				if ($isSaved = $this->edit()) $this->afterUpdate();
			}
		}
		else {
			$isSaved = false;
			//print_r($this->arrErrors);
		}
		
		// Если успешно сохранён объект, то вызываем метод afterSave
		return $isSaved;
	}
	
	/**
	 * Метод добавления новой записи в БД
	 * @return bool - true - запись добавилась в БД, false - нет
	 */
	protected function add() {
		$objDB = Service_Database::instanse();
		if (isset($this->arrSettings['created_at'])) $this->tableFields['created_at'] = gmdate("Y-m-d H:i:s", time());
		if (isset($this->arrSettings['author'])) $this->tableFields['ID_user'] = User_Seance::getId();
		
		if($currentID = $objDB->insertAutoincrement($this->tableName, $this->tableFields)) {
			$this->id = $currentID;
			return true;
		}
		else return false;
	}
	
	/**
	 * Метод редактирования записи в БД
	 * @return mix:  true - при успешном сохранении; array - при наличие ошибок в заполнении полей
	 */
	protected function edit() {
		$objDB = Service_Database::instanse();
		if (isset($this->arrSettings['updated_at'])) $this->tableFields['updated_at'] = gmdate("Y-m-d H:i:s", time());
		return $objDB->update($this->tableName, $this->tableFields, '`' . $this->pkName . "` = '" . $this->id . "'");
	}
	
	/**
	 * Метод выполняет проверку заполненных полей объекта по установленным правилам
	 * @return bool - флаг успешности валидации
	 */
	protected function validation() { 
		// Проверяем, все ли обязательные поля существуют
		foreach ($this->requiredFields as $status => $requiredField) {
			//if ($status == $requiredField) $status = null;
		//	echo $requiredField ;
			if ($status == 'create' && $this->saveType == 'update') continue;
			elseif($status == 'always' && isset($this->tableFields[$requiredField]) && $this->tableFields[$requiredField] == '' ) { // Если обязательного поля нет или оно содержит пустоту, то пишем ошибку
				$this->arrErrors[] = 'Переданы не все параметры';
				return false;
			}
		}
		// Валидируем каждое поле, все обязательные поля присутствуют!
		foreach ($this->tableFields as $fieldKey => $fieldValue) {
			if (isset($this->fieldsRules[$fieldKey])) {
				if (!$this->fieldValidation($fieldValue, $this->fieldsRules[$fieldKey])) $this->arrErrors[] = 'Поле ' . $fieldKey . ' не прошло валидацию';
			}
		}
		return empty($this->arrErrors);
	}
	
	/**
	* Метод возвращает значение поля
	*
	* @param string $fieldName
	* @return mixed
	*/
	public function getField($fieldName) {
		if (isset($this->tableFields[$fieldName])) return $this->tableFields[$fieldName];
		else return null;
	}

	/**
	 * Метод устанавливает одно значение поля
	 *
 	 * @param string $fieldName - имя
	 * @param string $fieldName - значение
	 */
	public function setField($fieldName, $fieldValue) {
		$this->tableFields[$fieldName] = $fieldValue; 
	}
	
	public function updateFields($arrFields) {
		if (!$this->id || !$arrFields) return false;
		$objDB = Service_Database::instanse();
		$stmt = "UPDATE `" . $this->tableName . "` SET ";
		$i = 0;
		foreach ($arrFields as $fieldName => $fieldValue) {
			if ($i++) $stmt .= ', ';
			if (is_bool($fieldValue)) $fieldValue = 0 + $fieldValue;
			if (is_int($fieldValue)) $fieldValue = '' . $fieldValue;
			$stmt .=  "`" . $this->tableName . "`.`" . $fieldName . "` = '" . Service_Escape::absolutelyEscapeString($fieldValue) . "'";
		}
		$stmt .= " WHERE `" . $this->tableName . "`.`" . $this->pkName . "` = '" . intval($this->id) . "'";
		return $objDB->query($stmt);
	}
	
	/**
	 * Метод устанавливает значения полей
	 *
	 * @param array $tableFields - массив данных
	 */
	public function setFields($tableFields) {
		foreach ($tableFields as $key => $value) $this->tableFields[$key] = $value;
	}
	
	protected function beforeValidation() {}
	
	protected function beforeValidationOnCreate() {}
	protected function beforeValidationOnUpdate() {}
	
	protected function beforeCreate() {}
	protected function beforeUpdate() {}
	
	protected function afterCreate() {}
	protected function afterUpdate() {}
	
	/**
	 * Функция осуществляет проверку полей согласно правилам
	 * @param mix $fieldValue - значение поля для проверки
	 * @param array $arrFieldRule - ассив правил, по которым проверяется текущее значение
	 */
	protected function fieldValidation($fieldValue, $arrFieldRule) {
		if($fieldValue == '' && isset($arrFieldRule['allowNull']) && $arrFieldRule['allowNull']) return true;
		if(isset($arrFieldRule['type'])) { // Если для поля указан тип - проверяем по маске
			switch ($arrFieldRule['type']) {
				case 'name':		// Имена и фамилии
					$reg = "/^[а-яa-z]*$/iu";
					break;
				case 'number':		// Строки содержащие только числа
					$reg = "/^[0-9]*$/i";
					break;
				case 'password':	// Пароли
					$reg = "/^[0-9a-z]{6,32}$/i";
					break;
				case 'email':		// Почта
					$reg = "/^([a-z0-9_\.\-]{1,20})@([a-z0-9\.\-]{1,20})\.([a-z]{2,4})$/i";
					break;
			}
			if(!preg_match($reg, $fieldValue)) return false;
		}
		
		$valueLength = Service_Function::utf8_strlen($fieldValue);
		if(isset($arrFieldRule['min']) && $valueLength < $arrFieldRule['min'])  return false;
		if(isset($arrFieldRule['max']) && $valueLength > $arrFieldRule['max'])  return false;
		
		return true;
	}
	
	/**
	 * Метод устанавливает свойство (поисковое)
	 *
	 * @param string $settingName
	 * @param mixed $settingValue
	 */
	public function setSetting($settingName, $settingValue) {
		switch ($settingName) {
			case 'select':
				$this->select = $settingValue;
				break;
			case 'conditions':
				$this->conditions = $settingValue;
				break;
			case 'order':
				$this->order = $settingValue;
				break;
			case 'group':
				$this->group = $settingValue;
				break;
			case 'limit':
				$this->limit = $settingValue;
				break;
			case 'having':
				$this->having = $settingValue;
				break;
			case 'joins':
				$this->joins = $settingValue;
				break;
			default:
				throw new Exception('You are trying to use invalid configuration ('. $settingName . '). The assume settings are: "select", "conditions", "joins", "include", "includeList", "order", "limit", "offset"');
				break;
		}
	}
	
	/**
	 * Метод формирования поисковой строки
	 *
	 */
	private function _makeStmt() {
		$this->stmt = "SELECT " . $this->select .
		" FROM `" . $this->tableName . "`" .
		($this->joins ? (" " . $this->joins) : null) .
		($this->conditions ? " WHERE " . $this->conditions : null) .
		($this->group ? " GROUP BY " . $this->group : null) .
		($this->having ? " HAVING " . $this->having : null) .
		($this->order ? " ORDER BY " . $this->order : null) .
		($this->limit ? " LIMIT " . $this->limit : null);
	}

}
?>